<?php // salespaymentdates.php // StemJrTest // 2013 // javier.rey.eu@gmail.com

/**
 * PURPOSE: Test developed by Javier Rey for Stemnet.
 * 
 * REQUIREMENT: Create a small command-line utility to help a fictional company determine the
 * dates they need to pay salaries to their sales department.
 * This company is handling their sales payroll in the following way:
 * - Sales staff get a regular monthly fixed base salary and a monthly bonus.
 * - The base salaries are paid on the last day of the month unless that day is a Saturday or a
 * Sunday (weekend).
 * - On the 15th of every month bonuses are paid for the previous month, unless that day is a
 * weekend. In that case, they are paid the first Wednesday after the 15th.
 * The output of the utility should be a CSV file, containing the payment dates for the
 * remainder of this year. The CSV file should contain a column for the month name, a
 * column that contains the salary payment date for that month, and a column that
 * contains the bonus payment date.
 * 
 * NOTE: The base salaries are paid before the last day of the month if it is a weekend.
 * NOTE: Today is included in the reminder unless the parameter "offset=1" is passed to the
 * command line, in which case, it is excluded.
 * NOTE: One single argument is optionally passed to the command line and interpreted as a set of
 * comma separated name=value pairs.
 * 
 * USAGE: salespaymentdates.php dbg=0,offset=0
 * PARAM dbg: Optional, default 0. Debug trace enabled (1) or disabled (0).
 * PARAM offset: Optional, default 0. Today is included in the reminder of the year. If day offset
 * is different from 0, the reminder is calculated from the current date plus the offset, positive
 * for future and negative for past. Thus, a value of 1 excludes today from the reminder.
 *
 * CLASSES: M, SalesPaymentDates, U, FileSystem
 * CLASS M: Main document class. It is a singleton that runs the script when instanced.
 * CLASS SalesPaymentDates: Required logic to calculate payment dates.
 * CLASS U: General purpose utilities.
 * CLASS FileSystem: FileSystem utilities.
 *
 */

/**
 * Class M: Main document class (singleton)
 */
class M {
	/**
 	 * Constants/Variables:
 	 */

	private static $FILE_NAME = "SalesPaymentDates"; // Def
	private static $FILE_EXT = ".csv"; // Def
	
	private $iSalesPaymentDates = NULL; // SalesPaymentDates instance
	private static $filePath; // System document path
	private static $urlPath; // Web document path, not required
	
	public static $cli = false; // CLI/Web
	
	public $input = NULL; // Input params
	public $output = NULL; // Output object

	/**
 	 * Constructor/Singleton:
 	 */

	private static $instance;
	public static function getInstance() { 
		if (!self::$instance) {self::$instance = new self();}
		return self::$instance;
	}
	private function __construct() {
		global $argv;
		
		M::$cli = (PHP_SAPI == "cli");
		M::$filePath = $_SERVER[SCRIPT_FILENAME];
		M::$urlPath = $_SERVER[PHP_SELF]; // Same as filePath in CLI mode
		if (M::$cli) { // Unique argument with comma separated name=value pairs:
			$this->input = U::requestObject($argv[1], ","); // Def ",", ";" // No "&"
		} else { // Web testing, not required:
			$this->input = $_GET;
		}
		
		U::$dbg = intval($this->input["dbg"]); // HIDE/SHOW (production/development)
	}

	/**
 	 * Flow:
 	 */

	public function run() {
		$this->iSalesPaymentDates = new SalesPaymentDates();
		
		$offset = intval($this->input["offset"]);
		$this->output = $this->iSalesPaymentDates->run($offset);
		
		$year = getdate();
		$year = $year['year'];
		$urlfolder = M::getFolder(M::$urlPath);
		$folder = M::getFolder(M::$filePath);
		$file = M::$FILE_NAME.$year.M::$FILE_EXT;
		
		$this->output = SalesPaymentDates::toGridString($this->output);
		$ref = FileSystem::saveFile($folder.$file, $this->output);
		
		if (M::$cli) {
			U::trace($folder.$file, "output file", TRUE);
		} else { // Web testing, not required:
			U::trace($urlfolder.$file, "output file", TRUE);
		}
		
		U::trace($this->input, "input");
		U::trace($this->output, "output");
		// U::trace($_SERVER, "SERVER"); // HIDE!
	}

	/**
 	 * Functions:
 	 */

	private static function getFolder($path) {
		$i = strrpos($path, "/");
		if (M::$cli && $i === FALSE) {$i = strrpos($path, "\\");}
		if (M::$cli && $i === FALSE) {$i = strrpos($path, ":");}
		if ($i !== FALSE) {$path = substr($path, 0, $i+1);}
		return $path;
	}
}

/**
 * Class SalesPaymentDates: Logic for calculating reminder payment dates in the current year
 */
class SalesPaymentDates {
	/**
 	 * Constants/Variables:
 	 */

	private static $CONTENT_COLUMNS = array("MONTH", "SALARY_DATE", "BONUS_DATE"); // Def

	/**
 	 * Constructor:
 	 */

	public function __construct() {
		// U::trace("SalesPaymentDates ini");
	}

	/**
 	 * Flow:
 	 */

	public function run($offset = 0) {
		$DAY_SECONDS = 24*60*60;
		$start = getdate(time()+$offset*$DAY_SECONDS); // Current date plus day offset: offset 0: today included in reminder, offset 1: today excluded from reminder
		$start = getdate($start[0]-$start['hours']*60*60-$start['minutes']*60-$start['seconds']); // Truncated day
		$end = getdate(strtotime(($start['year']+1).'-1-1')); // Next truncated year

		$format = 'D Y-m-d'; // Def 'D Y-m-d'...
		$months = 12;
		$out = array();
		for ($i = $start['mon']; $i < $months+1; $i++) {
			$month = getdate(strtotime($start['year'].'-'.$i.'-1'));
			$month = $month['month'];
			
			$salaryDate = "";
			if ($i == $months) {
				$salaryDate = getdate(strtotime(($start['year']+1).'-1-1')-$DAY_SECONDS); // Last day of December
			} else {
				$salaryDate = getdate(strtotime($start['year'].'-'.($i+1).'-1')-$DAY_SECONDS); // Last day of month
			}
			while ($salaryDate['wday'] == 0 || $salaryDate['wday'] == 6) { // While weekend
				$salaryDate = getdate($salaryDate[0]-$DAY_SECONDS); // Decrease 1 day
			}
			if ($salaryDate[0] >= $start[0] && $salaryDate[0] < $end[0]) {
				$salaryDate = date($format, $salaryDate[0]);
			} else {
				$salaryDate = "";
			}
			
			$bonusDate = getdate(strtotime($start['year'].'-'.$i.'-15'));
			if ($bonusDate['wday'] == 0 || $bonusDate['wday'] == 6) { // If weekend
				while ($bonusDate['wday'] != 3) { // While not Wednesday
					$bonusDate = getdate($bonusDate[0]+$DAY_SECONDS); // Increase 1 day
				}
			}
			if ($bonusDate[0] >= $start[0] && $bonusDate[0] < $end[0]) {
				$bonusDate = date($format, $bonusDate[0]);
			} else {
				$bonusDate = "";
			}
			
			if ($salaryDate || $bonusDate) {
				$out[] = array(self::$CONTENT_COLUMNS[0] => $month, self::$CONTENT_COLUMNS[1] => $salaryDate, self::$CONTENT_COLUMNS[2] => $bonusDate);
			}
		}

		// U::trace("SalesPaymentDates run ".date($format, $start[0])." ".date($format, $end[0]));
		return $out;
	}

	/**
 	 * Functions:
 	 */

	public static function toGridString($arr) {
		$q = "\""; // quotes: "\"", "'", ""
		$c = ","; // commas: ",", "\t"
		$n = PHP_EOL; // lines: PHP_EOL, "\r\n", "\n", "\r"
		$grid = $q.implode($q.$c.$q, self::$CONTENT_COLUMNS).$q.$n;
		for ($i = 0; $i < count($arr); $i++) {
			$line = '';
			foreach ($arr[$i] as $k=>$v) {
				if ($line) {$line .= $c;}
				$line .= $q.$v.$q;
			}
			$grid .= $line.$n;
		}
		return $grid;
	}
}

/**
 * Class U: Static general purpose utilities
 */
class U {
	/**
 	 * Constants/Variables:
 	 */

	public static $dbg = 0; // Debug 0, 1

	/**
 	 * Function requestObject: Converts a URLEncoded String set of params into an array Object
 	 */
	public static function requestObject($req, $sep = "&", $sep1 = "=") {
		if ($req === NULL) {$req = '';}
		$req = explode($sep, $req); $len = count($req); $obj = array();
		for ($i = 0; $i < $len; $i++) {
			if (!$req[$i]) {continue;}
			$j = strpos($req[$i], $sep1);
			if (!$j) {if ($j === false) {$req[$i] = $sep1.$req[$i];} $req[$i] = "par_".$i.$req[$i]; $j = strpos($req[$i], $sep1);}
			$n = substr($req[$i], 0, $j); $v = substr($req[$i], $j+1);
			$v = rawurldecode($v); // SHOW/HIDE
			// if (strrpos($v, $sep1) !== strlen($v)-strlen($sep1)) {$v = requestObject($v, $sep, $sep1);} // HIDE/SHOW
			$obj[$n] = $v;
		}
		return $obj;
	}

	/**
 	 * Function trace: Echoes text and converts it into HTML source if not CLI mode
 	 */
	public static function trace($text, $title = "", $force = FALSE) {
		if (!self::$dbg && !$force) {return;}
		$text = print_r($text, TRUE);
		$sep = " "; if (strpos($text, "\n") !== FALSE) {$sep = "\n";}
		$text = "\n".$title.":".$sep.$text;
		if (PHP_SAPI != "cli") {
 			// $text = str_replace("&", "&amp;", $text); // HIDE/SHOW
 			$text = str_replace("<", "&lt;", $text);
 			$text = str_replace("\n", "<br/>\n", $text);
		}
		echo $text;
	}
}

/**
 * Class FileSystem: File system utilities for CLI mode
 */
class FileSystem {
	/**
 	 * Function saveFile: Creates a file or overwrites an existing file with string content.
 	 */
	public static function saveFile($file, $content) {
		$ref = fopen($file, "w+"); // U::trace("saveFile open: ".$ref);
		$ret = fwrite($ref, "".$content); // U::trace("saveFile write: ".$ret);
		$ret = fclose($ref); // U::trace("saveFile close: ".$ret);
		return $ref;
	}
}

// Run document:

$main = M::getInstance();
$main->run();

?>